import Element from './Element.js'

export default class DisplayElement extends Element {
  // A general class that handles dealing with screen coordinates, the tree
  // of elements, and other common stuff.
  //
  // This element doesn't handle any real rendering; just layouts. Placing
  // characters at specific positions should be implemented in subclasses.
  //
  // It's a subclass of EventEmitter, so you can make your own events within
  // the logic of your subclass.

  static drawValues = Symbol('drawValues')
  static lastDrawValues = Symbol('lastDrawValues')
  static scheduledDraw = Symbol('scheduledDraw')

  constructor() {
    super()

    this[DisplayElement.drawValues] = {}
    this[DisplayElement.lastDrawValues] = {}
    this[DisplayElement.scheduledDraw] = false

    this.visible = true

    this.x = 0
    this.y = 0
    this.w = 0
    this.h = 0

    this.hPadding = 0
    this.vPadding = 0

    // Note! This only applies to the parent, not the children. Useful for
    // when you want an element to cover the whole screen but allow mouse
    // events to pass through.
    this.clickThrough = false
  }

  drawTo(writable) {
    // Writes text to a "writable" - an object that has a "write" method.
    // Custom rendering should be handled as an override of this method in
    // subclasses of DisplayElement.
  }

  renderTo(writable) {
    // Like drawTo, but only calls drawTo if the element is visible. Use this
    // with your root element, not drawTo.

    if (!this.visible) {
      return
    }

    const causeRenderEl = this.shouldRender()
    if (causeRenderEl) {
      this.drawTo(writable)
      this.renderChildrenTo(writable)
      this.didRenderTo(writable)
    } else {
      this.renderChildrenTo(writable)
    }
  }

  shouldRender() {
    // WIP! Until this implementation is finished, always return true (or else
    // lots of rendering breaks).
    /*
    return (
      this[DisplayElement.scheduledDraw] ||
      [...this.directAncestors].find(el => el.shouldRender())
    )
    */
    return true
  }

  renderChildrenTo(writable) {
    // Renders all of the children to a writable.

    for (const child of this.children) {
      child.renderTo(writable)
    }
  }

  didRenderTo(writable) {
    // Called immediately after rendering this element AND all of its
    // children. If you need to do something when that happens, override this
    // method in your subclass.
    //
    // It's fine to draw more things to the writable here - just keep in mind
    // that it'll be drawn over this element and its children, but not any
    // elements drawn in the future.
  }

  fixLayout() {
    // Adjusts the layout of children in this element. If your subclass has
    // any children in it, you should override this method.
  }

  fixAllLayout() {
    // Runs fixLayout on this as well as all children.

    this.fixLayout()
    for (const child of this.children) {
      child.fixAllLayout()
    }
  }

  confirmDrawValuesExists() {
    if (!this[DisplayElement.drawValues]) {
      this[DisplayElement.drawValues] = {}
    }
  }

  getDep(key) {
    this.confirmDrawValuesExists()
    return this[DisplayElement.drawValues][key]
  }

  setDep(key, value) {
    this.confirmDrawValuesExists()
    const oldValue = this[DisplayElement.drawValues][key]
    if (value !== this[DisplayElement.drawValues][key]) {
      this[DisplayElement.drawValues][key] = value
      this.scheduleDraw()
      // Grumble: technically it's possible for a root element to not be an
      // actual Root. While we don't check for this case most of the time (even
      // though we ought to), we do here because it's not unlikely for draw
      // dependency values to be changed before the element is actually added
      // to a Root element.
      if (this.root.scheduleRender) {
        this.root.scheduleRender()
      }
    }
    return value
  }

  scheduleDrawWithoutPropertyChange() {
    // Utility function for when you need to schedule a draw without updating
    // any particular draw-dependency property on the element. Works by setting
    // an otherwise unused dep to a unique object. (We can't use a symbol here,
    // because then Object.entries doesn't notice it.)
    this.setDep('drawWithoutProperty', Math.random())
  }

  scheduleDraw() {
    this[DisplayElement.scheduledDraw] = true
  }

  unscheduleDraw() {
    this[DisplayElement.scheduledDraw] = false
  }

  hasScheduledDraw() {
    if (this[DisplayElement.scheduledDraw]) {
      for (const [ key, value ] of Object.entries(this[DisplayElement.drawValues])) {
        if (value !== this[DisplayElement.lastDrawValues][key]) {
          return true
        }
      }
    }
    return false
  }

  updateLastDrawValues() {
    Object.assign(this[DisplayElement.lastDrawValues], this[DisplayElement.drawValues])
  }

  centerInParent() {
    // Utility function to center this element in its parent. Must be called
    // only when it has a parent. Set the width and height of the element
    // before centering it!

    if (this.parent === null) {
      throw new Error('Cannot center in parent when parent is null')
    }

    this.x = Math.round((this.parent.contentW - this.w) / 2)
    this.y = Math.round((this.parent.contentH - this.h) / 2)
  }

  fillParent() {
    // Utility function to fill this element in its parent. Must be called
    // only when it has a parent.

    if (this.parent === null) {
      throw new Error('Cannot fill parent when parent is null')
    }

    this.x = 0
    this.y = 0
    this.w = this.parent.contentW
    this.h = this.parent.contentH
  }

  fitToParent() {
    // Utility function to position this element so that it stays within its
    // parent's bounds. Must be called only when it has a parent.
    //
    // This function is useful when (and only when) the right or bottom edge
    // of this element may be past the right or bottom edge of its parent.
    // In such a case, the element will first be moved left or up by the
    // distance that its edge exceeds that of its parent, so that its edge is
    // no longer past the parent's. Then, if the left or top edge of the
    // element is less than zero, i.e. outside the parent, it is set to zero
    // and the element's width or height is adjusted so that it does not go
    // past the bounds of the parent.

    if (this.x + this.w > this.parent.right) {
      const offendExtent = (this.x + this.w) - this.parent.contentW
      this.x -= offendExtent
      if (this.x < 0) {
        const offstartExtent = 0 - this.x
        this.w -= offstartExtent
        this.x = 0
      }
    }

    if (this.y + this.h > this.parent.bottom) {
      const offendExtent = (this.y + this.h) - this.parent.contentH
      this.y -= offendExtent
      if (this.y < 0) {
        const offstartExtent = 0 - this.y
        this.h -= offstartExtent
        this.y = 0
      }
    }
  }

  getElementAt(x, y) {
    // Gets the topmost element at the provided absolute coordinate.
    // Note that elements which are not visible or have the clickThrough
    // property set to true are not considered.

    const children = this.children.slice()

    // Start searching the last- (top-) rendered children first.
    children.reverse()

    for (const el of children) {
      if (!el.visible) {
        continue
      }

      const el2 = el.getElementAt(x, y)
      if (el2) {
        return el2
      }

      if (el.clickThrough) {
        continue
      }

      const { absX, absY, w, h } = el
      if (absX <= x && absX + w > x) {
        if (absY <= y && absY + h > y) {
          return el
        }
      }
    }
    return null
  }

  get x() { return this.getDep('x') }
  set x(v) { return this.setDep('x', v) }
  get y() { return this.getDep('y') }
  set y(v) { return this.setDep('y', v) }
  get hPadding() { return this.getDep('hPadding') }
  set hPadding(v) { return this.setDep('hPadding', v) }
  get vPadding() { return this.getDep('vPadding') }
  set vPadding(v) { return this.setDep('vPadding', v) }
  get visible() { return this.getDep('visible') }
  set visible(v) { return this.setDep('visible', v) }

  // Commented out because this doesn't fix any problems (at least ATM).
  // get parent() { return this.getDep('parent') }
  // set parent(v) { return this.setDep('parent', v) }

  get absX() {
    if (this.parent) {
      return this.parent.contentX + this.x
    } else {
      return this.x
    }
  }

  get absY() {
    if (this.parent) {
      return this.parent.contentY + this.y
    } else {
      return this.y
    }
  }

  // Where contents should be positioned.
  get contentX() { return this.absX + this.hPadding }
  get contentY() { return this.absY + this.vPadding }
  get contentW() { return this.w - this.hPadding * 2 }
  get contentH() { return this.h - this.vPadding * 2 }

  get left()   { return this.x }
  get right()  { return this.x + this.w }
  get top()    { return this.y }
  get bottom() { return this.y + this.h }

  get absLeft()   { return this.absX }
  get absRight()  { return this.absX + this.w - 1 }
  get absTop()    { return this.absY }
  get absBottom() { return this.absY + this.h - 1 }
}
